<!DOCTYPE html>
<html>
  <head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
  <meta name="description" content="tong.li&#39;s blog">
  <meta name="keyword" content="彤哥哥博客，95后技术爱好者,现就职于同程旅行/同程艺龙上海分公司，专注于互联网技术分享的平台。">
  
    <link rel="shortcut icon" href="/css/images/icon.png">
  
  <title>
    
      跟着彤哥学网络-传输层TCP&amp;UDP协议 | 彤哥哥的博客
    
  </title>
  <link href="https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
  <link href="https://cdn.staticfile.org/nprogress/0.2.0/nprogress.min.css" rel="stylesheet">
  <link href="https://cdn.staticfile.org/highlight.js/9.12.0/styles/tomorrow-night.min.css" rel="stylesheet">
  
<link rel="stylesheet" href="/css/style.css">

  
  <script src="https://cdn.staticfile.org/jquery/3.2.1/jquery.min.js"></script>
  <script src="https://cdn.staticfile.org/geopattern/1.2.3/js/geopattern.min.js"></script>
  <script src="https://cdn.staticfile.org/nprogress/0.2.0/nprogress.min.js"></script>
  
    
<script src="/js/qrious.js"></script>

  
  
  
  
    <!-- MathJax support START -->
    <script type="text/x-mathjax-config">
      MathJax.Hub.Config({
        tex2jax: {
          inlineMath: [ ['$','$'], ["\\(","\\)"]  ],
          processEscapes: true,
          skipTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
        }
      });
    </script>

    <script type="text/x-mathjax-config">
      MathJax.Hub.Queue(function() {
        var all = MathJax.Hub.getAllJax(), i;
        for (i=0; i < all.length; i += 1) {
          all[i].SourceElement().parentNode.className += ' has-jax';
        }
      });
    </script>
    <script type="text/javascript" src="https://cdn.staticfile.org/mathjax/2.7.5/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
    <!-- MathJax support END -->
  


  
  
    
<script src="/js/local-search.js"></script>


<meta name="generator" content="Hexo 5.4.2"></head>
<div class="wechat-share">
  <img src="/css/images/logo.png" />
</div>
  <body>
    <header class="header fixed-header">
  <div class="header-container">
    <a class="home-link" href="/">
      <div class="logo"></div>
      <span>彤哥哥的博客</span>
    </a>
    <ul class="right-list">
      
        <li class="list-item">
          
            <a href="/" class="item-link">主页</a>
          
        </li>
      
        <li class="list-item">
          
            <a href="/series/" class="item-link">分类</a>
          
        </li>
      
        <li class="list-item">
          
            <a href="/tags/" class="item-link">标签</a>
          
        </li>
      
        <li class="list-item">
          
            <a href="/archives/" class="item-link">归档</a>
          
        </li>
      
        <li class="list-item">
          
            <a href="/project/" class="item-link">项目</a>
          
        </li>
      
        <li class="list-item">
          
            <a href="/about/" class="item-link">关于</a>
          
        </li>
      
      
        <li class="menu-item menu-item-search right-list">
    <a role="button" class="popup-trigger">
        <i class="fa fa-search fa-fw"></i>
    </a>
</li>
      
    </ul>
    <div class="menu">
      <span class="icon-bar"></span>
      <span class="icon-bar"></span>
      <span class="icon-bar"></span>
    </div>
    <div class="menu-mask">
      <ul class="menu-list">
        
          <li class="menu-item">
            
              <a href="/" class="menu-link">主页</a>
            
          </li>
        
          <li class="menu-item">
            
              <a href="/series/" class="menu-link">分类</a>
            
          </li>
        
          <li class="menu-item">
            
              <a href="/tags/" class="menu-link">标签</a>
            
          </li>
        
          <li class="menu-item">
            
              <a href="/archives/" class="menu-link">归档</a>
            
          </li>
        
          <li class="menu-item">
            
              <a href="/project/" class="menu-link">项目</a>
            
          </li>
        
          <li class="menu-item">
            
              <a href="/about/" class="menu-link">关于</a>
            
          </li>
        
      </ul>
    </div>
    
      <div class="search-pop-overlay">
    <div class="popup search-popup">
        <div class="search-header">
            <span class="search-icon">
                <i class="fa fa-search"></i>
            </span>
            <div class="search-input-container">
                <input autocomplete="off" autocapitalize="off"
                    placeholder="Please enter your keyword(s) to search." spellcheck="false"
                    type="search" class="search-input">
            </div>
            <span class="popup-btn-close">
                <i class="fa fa-times-circle"></i>
            </span>
        </div>
        <div id="search-result">
            <div id="no-result">
                <i class="fa fa-spinner fa-pulse fa-5x fa-fw"></i>
            </div>
        </div>
    </div>
</div>
    
  </div>
</header>

    <div id="article-banner">
  <h2>跟着彤哥学网络-传输层TCP&UDP协议</h2>
  <p class="post-date">2021-09-05</p>
  <div class="arrow-down">
    <a href="javascript:;"></a>
  </div>
</div>
<main class="app-body flex-box">
  <!-- Article START -->
  <article class="post-article">
    <section class="markdown-content"><h1 id="1-传输层概述"><a href="#1-传输层概述" class="headerlink" title="1. 传输层概述"></a>1. 传输层概述</h1><p><img src="https://ltyeamin.github.io/imgs/2021/09/20210904145021.png"></p>
<p>传输层是计算机网络OSI七层模型中关键层次之一，着重理解如下几点即可：</p>
<ol>
<li>传输层为相互通信的应用进程提供逻辑通信。</li>
<li>IP地址和端口号组成了我们所谓的套接字Socket。</li>
<li>TCP协议是面向可靠性连接、分段编号进行传送、含有流量控制功能的协议，它可以建立会话，为上层协议提供数据传输的能力。</li>
<li>UDP协议是面向不可靠性的连接协议，不建立会话，一个数据包就能完成数据通信。</li>
<li>在不可靠的网络上要想实现可靠性传输，需要停止等待协议和ARQ协议。</li>
<li>TCP的滑动窗口、流量控制、拥塞控制和连接管理。</li>
<li>应用层将数据传递到传输层分组成TCP报文段/UDP数据包，再将TCP报文段/UDP数据包传输到网络层，加上IP首部后成为IP包，再将IP包传输到数据链路层，加上Mac地址等其他首部和尾部后成为以太网数据帧，最终通过物理设备以比特流的形式传送数据。</li>
</ol>
<h1 id="2-TCP-IP协议"><a href="#2-TCP-IP协议" class="headerlink" title="2. TCP/IP协议"></a>2. TCP/IP协议</h1><h2 id="2-1-TCP及套接字"><a href="#2-1-TCP及套接字" class="headerlink" title="2.1 TCP及套接字"></a>2.1 TCP及套接字</h2><p><img src="https://ltyeamin.github.io/imgs/2021/09/20210904145635.png"></p>
<p>TCP(Transmission Control Protocol)TCP位于TCP/IP模型的传输层，它是一种面向连接的端到端协议。 </p>
<p>TCP作为传输控制协议，可以为主机提供可靠的数据传输。如上图所示，两台主机在通信之前，需要TCP在它们之间建立可靠的传输通道。</p>
<p><img src="https://ltyeamin.github.io/imgs/2021/09/20210904152320.png"></p>
<p>TCP允许一个主机同时运行多个应用进程。每台主机可以拥有多个应用端口，每对端口号、源和目标IP地址的组合唯一地标识了一个会话。</p>
<p>端口分为知名端口和动态端口。有些网络服务会使用固定的端口，这类端口称知名端口，端口号范围为0-1023。如FTP、 HTTP、Telnet、 SNMP服务均使用知名端口。</p>
<p>动态端口号范围从1024到65535，这些端口号一般不固定分配给某个服务，也就是说许多服务都可以使用这些端口。只要运行的程序向系统提出访问网络的申请，那么系统就可以从这些端口号中分配一个供该程序使用。</p>
<blockquote>
<p>套接字Socket = IP地址: 端口号</p>
<p>TCP连接 ::= {Socket1,Socket2} = {( IP地址1: 端口号1),( IP地址2: 端口号2)}</p>
</blockquote>
<h2 id="2-2-TCP主要特点"><a href="#2-2-TCP主要特点" class="headerlink" title="2.2 TCP主要特点"></a>2.2 TCP主要特点</h2><ul>
<li><p>TCP是面向连接的运输层协议。应用程序在使用TCP协议之前必须先建立连接。在使用之后要释放连接。可以想象成打电话，通话之前要拨号建立连接，通过之后要挂断释放连接。</p>
</li>
<li><p>每条TCP连接只能有两个断点(endponit),每条TCP连接只能是点对点的。</p>
</li>
<li><p>TCP是可靠性连接。通过TCP连接传送的数据，无差错、不丢失、不重复、并且按序到达。</p>
</li>
<li><p>TCP是全双工通信，即在任何时候TCP允许双方发送数据。TCP连接两端都设有发送缓存和接收缓存，用来临时存放双向通信的数据。在发送时，应用程序在把数据传送给TCP的缓存后，就可以做自己的事，而TCP在合适的时候把数据发送出去。在接收时，TCP把收到的数据放入缓存，上层的应用进程在合适的时候读取缓存中的数据。</p>
</li>
<li><p>面向<strong>字节流</strong>的数据传输。TCP中的流(stream)指的是流入到进程或从进程流出的字节序列。接收方应用程序收到的字节流必须和发送方应用程序发出的字节流完全一样,接收方才有能力辨别且交给上层应用程序。我们经常所说的半包、粘包问题就是这里的概念。</p>
</li>
</ul>
<h2 id="2-3-TCP头部"><a href="#2-3-TCP头部" class="headerlink" title="2.3 TCP头部"></a>2.3 TCP头部</h2><p>TCP虽然是面向字节流的，但TCP传送的数据单元却是报文段。</p>
<p>TCP通常使用IP作为网络层协议，这时TCP数据段被封装在IP数据包内.</p>
<p>TCP数据段由TCP Header（头部）和TCP Data（数据）组成。 TCP最多可以有60个字节的头部，如果没有Options字段，正常的固定长度是20字节。</p>
<p><img src="https://ltyeamin.github.io/imgs/2021/09/20210904153640.png"></p>
<ul>
<li>Source Port：源端口号，占16位，源主机应用程序使用的端口号。</li>
<li>Destination Port：目的端口号，占16位，目的主机应用程序所使用的端口号。每个TCP头部都包含源和目的端的端口号，这两个值加上IP头部中的源IP地址和目的IP地址可以唯一确定一个TCP连接。</li>
<li>Sequence Number：32位序列号，用于标识从发送端发出的不同TCP数据段的序号。数据段在网络中传输时，它们的顺序可能会发生变化；接收端依据此序列号，便可按照正确的顺序重组数据。</li>
<li>Acknowledge Number：32位确认序列号，用于标识接收端确认收到的数据段。确认序列号为成功收到的数据序列号加1。</li>
<li>Header Length：4位头部长度，表示头部占32bit字的数目，它能表达的TCP头部最大长度为60字节。</li>
<li>数据偏移：占4位，指出TCP报文段的首部长度。由于首部中还有长度不确定的选项字段，因此数据偏移字段是必要的。数据偏移的最大值是60字节，这也是TCP首部的最大长度。</li>
<li>保留：占6位，保留为今后使用，但目前应置为0。</li>
<li>URG：紧急URGent，当URG = 1时，表明紧急指针字段有效。它告诉系统此报文段中有紧急数据，应尽快传送(相当于高优先级的数据)，而不要按原来的排队顺序来传送。</li>
<li>确认ACK：仅当ACK = 1时确认号字段才有效。当ACK = 0时，确认号无效。TCP规定，在连接建立后所有传送的报文段都必须把ACK置1。</li>
<li>推送PSH：两个应用进程进行交互式的通信时，有时在一端的应用进程希望在键入一个命令后立即就能够收到对方的响应。在这种情况下，TCP就可以使用推送(push)操作。这时，发送方TCP把PSH置1，并立即创建一个报文段发送出去。接收方TCP收到PSH = 1的报文段，就尽快地（即“推送”向前）交付接收应用进程，而不再等到整个缓存都填满了后再向上交付。不太常用。</li>
<li>复位RST：RST = 1时，表明TCP连接中出现严重差错（如由于主机崩溃或其他原因），必须释放连接，然后再重新建立运输连接。RST置1还用来拒绝一个非法的报文段或拒绝打开一个连接。RST也可称为重建位或重置位。</li>
<li>同步SYN：在连接建立时用来同步序号。当SYN = 1而ACK= 0时，表明这是一个连接请求报文段。对方若同意建立连接，则应在响应的报文段中使SYN = 1和ACK = 1。</li>
<li>终止FIN：当FIN = 1时，表明此报文段的发送方的数据已发送完毕，并要求释放运输连接。</li>
<li>Windows：16位窗口大小，表示接收端期望通过单次确认而收到的数据的大小。窗口大小的最大值为65535字节，该机制通常用来进行流量控制。</li>
<li>Checksum：16位的校验和，校验整个TCP报文段，包括TCP头部和TCP数据。该值由发送端计算和记录并由接收端进行验证。</li>
<li>Urgent Ponit：紧急指针，占2字节。紧急指针仅在URG = 1时才有意义，它指出本报文段中的紧急数据的字节数（紧急数据结束后就是普通数据）。因此，紧急指针指出了紧急数据的末尾在报文段中的位置。当所有紧急数据都处理完时，TCP就告诉应用程序恢复到正常操作。值得注意的是，即使窗口为零时也可发送紧急数据。</li>
<li>Option选项：长度可变，最长可达40字节。当没有使用“选项”时，TCP的首部长度是20字节。最大报文段长度 MSS (Maximum Segment Size)是每一个TCP报文段中的<strong>数据字段</strong>的最大长度。假设TCP报文段含有1字节的数据，若选择较小的MSS长度，网络的利用率就很低。假设TCP报文段含有很多字节的数据，若选择较大的MSS长度，在网络层传输时会分成多组数据段，开销很大。因此MSS尽可能大点（只有不在网络层分片的长度即可）。不同的网络路由注定有分片的路径和不分片的路径，MSS值很难确认，因此在两端建立连接的时候，双方都要写入自己可以支持的MSS值，这样后续可以按照这个约定传送数据。若不写入，默认主机写入MSS默认值536字节，因特网上的主机都应能接受的报文段长度是536 +20（固定首部长度）= 556字节。MTU（最大传输单元） = MSS + TCP/UDP 首部⻓度+ IP首部⻓度</li>
<li>扩大窗口：后续增加选项，占3字节，TCP首部中窗口字段长度是16位，因此最大的窗口大小为64K字节。虽然这对早期的网络是足够用的，但对于包含卫星信道的网络，传播时延和带宽都很大，要获得高吞吐率需要更大的窗口大小。</li>
<li>时间戳：后续增加选项，占10字节，计算往返时间RTT，用于处理TCP序号超过2^32的情况，这又称为防止序号绕回 PAWS (ProtectAgainst Wrapped Sequence numbers)。</li>
</ul>
<p><img src="https://ltyeamin.github.io/imgs/2021/09/20210904153850.png"></p>
<h2 id="2-4-连接管理"><a href="#2-4-连接管理" class="headerlink" title="2.4 连接管理"></a>2.4 连接管理</h2><h3 id="2-4-1-三次握手连接"><a href="#2-4-1-三次握手连接" class="headerlink" title="2.4.1 三次握手连接"></a>2.4.1 三次握手连接</h3><p><img src="https://ltyeamin.github.io/imgs/2021/09/20210904205536.png"></p>
<p>最初客户端主机A与服务端主机A的会话状态为CLOSE状态。服务器A的进程先创建传输控制块TCB(Transmission Control Block,存储每个TCP连接重要信息，比如：TCP连接表，到发送和接收缓存的指针，到重传队列的指针，当前的发送和接收序号)，此时服务器A的会话状态为LISTEN状态，等待客户端主机A的连接请求。</p>
<ol start="2">
<li>客户端主机A也会创建TCB，然后向服务器A发出连接请求报文段。客户端会发送一个标识SYN=1的数据段，表示期望与服务器A建立连接，此数据段的序列号seq=a。TCP规定，SYN报文段（即SYN = 1的报文段）不能携带数据，但要消耗掉一个序号。这时，TCP客户进程进入SYN-SENT（同步已发送）状态。</li>
<li>服务器A收到连接请求报文段后，如同意建立连接，则向客户端主机A发送标识了SYN+ACK的数据段。此时seq=b，ack=a+1,以此作为对主机A的SYN报文的确认。请注意，这个报文段也不能携带数据，但同样要消耗掉一个序号。这时TCP服务器进程进入SYN-RCVD（同步收到）状态。</li>
<li>客户端主机A收到服务器A的确认后，还要给服务器A给出确认。此时发送ACK标识，seq=a+1，ack=b+1。TCP的标准规定，ACK报文段可以携带数据。但如果不携带数据则不消耗序号，在这种情况下，下一个数据报文段的序号仍是seq = a + 1。这时，TCP连接已经建立，A进入ESTABLISHED（已建立连接）状态。当B收到A的确认后，也进入ESTABLISHED状态。</li>
</ol>
<blockquote>
<p>小思考：为什么建立连接不是两次握手呢？为什么客户端主机A最后还要发送一次确认呢？</p>
<p>这主要是为了防止已失效的连接请求报文段突然又传送到了服务器A，因而产生错误。</p>
<p>客户端主机A发出连接请求，但因连接请求报文丢失而未收到确认。于是客户端主机A再重传一次连接请求。后来收到了确认，建立了连接。数据传输完毕后，就释放了连接。客户端主机A共发送了两个连接请求报文段，其中第一个丢失，第二个到达了服务器A。没有“已失效的连接请求报文段”。</p>
<p>现假设一种异常情况下网络通讯的路由时间较长，以致延误到连接释放以后的某个时间才到达服务器A，本来这是一个早已失效的报文段。但服务器A收到此失效的连接请求报文段后，就误认为是客户端A又发出一次新的连接请求。但服务器A收到此失效的连接请求报文段后，就误认为是客户端A又发出一次新的连接请求。于是就向客户端A发出确认报文段，同意建立连接。假定不采用三次握手，那么只要服务器A发出确认，新的连接就建立了。</p>
<p>由于现在客户端主机A并没有发出建立连接的请求，因此不会理睬服务器A的确认，也不会向服务器A发送数据。但服务器A却以为新的运输连接已经建立了，并一直等待客户端主机A发来数据。服务器A的许多资源就这样白白浪费了。</p>
<p>因此采用三次握手的办法可以防止上述现象的发生。</p>
</blockquote>
<h3 id="2-4-2-TCP传输过程"><a href="#2-4-2-TCP传输过程" class="headerlink" title="2.4.2  TCP传输过程"></a>2.4.2  TCP传输过程</h3><p><img src="https://ltyeamin.github.io/imgs/2021/09/20210904212107.png"></p>
<p>TCP的可靠传输还体现在TCP使用了确认技术来确保目的设备收到了从源设备发来的数据，并且是准确无误的。其工作原理如下：</p>
<ul>
<li>目的设备接收到源设备发送的数据段时，会向源端发送确认报文，源设备收到确认报文后，继续发送数据段，如此重复。</li>
</ul>
<p>如图所示，主机A向服务器A发送TCP数据段，为描述方便假定每个数据段的长度都是500个字节。</p>
<p>当服务器A成功收到序列号是M+1499的字节以及之前的所有字节时，会以序列号M+1499+1=M+1500进行确认。</p>
<p>另外，由于数据段N+3传输失败，所以服务器A未能收到序列号为M+1500的字节，因此服务器A还会再次以序列号M+1500进行确认。</p>
<h3 id="2-4-3-四次握手释放"><a href="#2-4-3-四次握手释放" class="headerlink" title="2.4.3  四次握手释放"></a>2.4.3  四次握手释放</h3><p><img src="https://ltyeamin.github.io/imgs/2021/09/20210904212835.png"></p>
<p>TCP支持全双工模式传输数据，这意味着同一时刻两个方向都可以进行数据的传输。</p>
<p>在传输数据之前， TCP通过三次握手建立的实际上是两个方向的连接，因此在传输完毕后，两个方向的连接也都必须关闭。</p>
<p>TCP连接的建立是一个三次握手的过程，而TCP连接的终止则要经过四次握手。</p>
<p>数据传输结束后，通信的双方都可释放连接。现在客户端主机A和服务器A都处于ESTABLISHED状态。</p>
<ol>
<li>主机A想终止连接，于是发送一个标识了FIN， ACK的数据段，序列号seq为a，确认序列号ack为b。此时主机A处于FIN-WAIT-1（终止等待1）状态，等服务器A确认。TCP规定，FIN报文段即使不携带数据，它也消耗掉一个序号。</li>
<li>服务器A回应一个标识了ACK的数据段，序列号seq为b，确认序号ack为a+1，作为对主机A的FIN报文的确认。此时服务器B处于CLOSE-WAIT（关闭等待）状态。TCP服务器进程这时应通知高层应用进程，因而从主机A到服务器A这个方向的连接就释放了，这时的TCP连接处于半关闭(half-close)状态，即主机A已经没有数据要发送了，但服务器A若发送数据，A仍要接收。也就是说，从服务器A到主机A这个方向的连接并未关闭，这个状态可能会持续一些时间。</li>
<li>服务器A想终止连接，于是向主机A发送一个标识了FIN， ACK的数据段，序列号seq为b，确认序列号ack为a+1。主机A收到来自服务器A的确认后，就进入FIN-WAIT-2（终止等待2）状态，等待服务器发出的连接释放。</li>
<li>主机A回应一个标识了ACK的数据段，序列号seq为a+1，确认序号ack为b+1，作为对服务器A的FIN报文的确认。请注意，现在TCP连接还没有释放掉。必须经过时间等待计时器(TIME-WAIT timer)设置的时间2MSL后，主机A才进入到CLOSED状态。</li>
</ol>
<blockquote>
<p>扩展小知识：</p>
<p>​     时间MSL叫做最长报文段寿命(Maximum Segment Lifetime)，RFC 793建议设为2分钟。但这完全是从工程上来考虑，对于现在的网络，MSL = 2分钟可能太长了一些。因此TCP允许不同的实现可根据具体情况使用更小的MSL值。因此，从主机A进入到TIME-WAIT状态后，要经过4分钟才能进入到CLOSED状态，才能开始建立下一个新的连接。当主机A撤销相应的传输控制块TCB后，就结束了这次的TCP连接。</p>
<p>​    除时间等待计时器外，TCP还设有一个保活计时器(keepalive timer)。设想有这样的情况：客户已主动与服务器建立了TCP连接。但后来客户端的主机突然出故障。显然，服务器以后就不能再收到客户发来的数据。因此，应当有措施使服务器不要再白白等待下去。这就是使用保活计时器。服务器每收到一次客户的数据，就重新设置保活计时器，时间的设置通常是两小时。若两小时没有收到客户的数据，服务器就发送一个探测报文段，以后则每隔75分钟发送一次。若一连发送10个探测报文段后仍无客户的响应，服务器就认为客户端出了故障，接着就关闭这个连接。</p>
</blockquote>
<blockquote>
<p>小思考1：为什么A在TIME-WAIT状态必须等待2MSL的时间呢？</p>
<ul>
<li>为了保证主机A发送的最后一个ACK报文段能够到达服务器A。这个ACK报文段有可能丢失，因而使处在LAST-ACK状态的服务器A收不到对已发送的FIN + ACK报文段的确认。服务器A会超时重传这个FIN + ACK报文段，而主机A就能在2MSL时间内收到这个重传的FIN + ACK报文段。接着主机A重传一次确认，重新启动2MSL计时器。最后，主机A和服务器A都正常进入到CLOSED状态。如果主机A在TIME-WAIT状态不等待一段时间，而是在发送完ACK报文段后立即释放连接，那么就无法收到服务器A重传的FIN + ACK报文段，因而也不会再发送一次确认报文段。这样，服务器A就无法按照正常步骤进入CLOSED状态。</li>
<li>防止上文提到的“已失效的连接请求报文段”出现在本连接中。主机A在发送完最后一个ACK报文段后，再经过时间2MSL，就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。服务器A只要收到了主机A发出的确认，就进入CLOSED状态。同样，服务器A在撤销相应的传输控制块TCB后，就结束了这次的TCP连接。我们注意到，服务器A结束TCP连接的时间要比主机A早一些。</li>
</ul>
<p>小思考2：为什么释放连接是四次连接？</p>
<p>我们都知道，TCP连接是全双工通信的，而断开时双方都需要确定两个问题：自己是否还有数据要发送，对方端是否还有数据要发送，而四次挥手正好在双方同步了这两个问题。</p>
<p>关闭连接时，客户端向服务端发送 <code>FIN</code> 时，仅仅表示客户端不再发送数据了但是还能接收数据。此时连接变成了半双工状态。</p>
<p>服务器收到客户端的 <code>FIN</code> 报文时，先回一个 <code>ACK</code> 应答报文，而服务端可能还有数据需要处理和发送，等服务端不再发送数据时，才发送 <code>FIN</code> 报文给客户端来表示同意现在关闭连接。</p>
<ul>
<li>第一次挥手：client告诉server自己的数据已全部发送，client可以回收发送缓冲区，server可以回收接收缓冲区</li>
<li>第二次挥手：server告诉client自己收到了关闭信息</li>
<li>第三次挥手：server告诉client自己的数据已全部发送，server可以回收发送缓冲区，client可以回收接收缓冲区</li>
<li>第四次挥手：client告诉server自己收到了关闭信息</li>
</ul>
<p>可以发现，四次挥手同样一次都不能少，<strong>如果少了其中任何一次，总有一方不能可靠的通知对方自己的数据已发送完毕</strong>，因此，连接不可能可靠的断开，TCP也就成了不可靠的协议。</p>
</blockquote>
<p><img src="https://ltyeamin.github.io/imgs/2021/09/20210906112418.png" alt="TCP的有限状态机"></p>
<h2 id="2-5-流量控制"><a href="#2-5-流量控制" class="headerlink" title="2.5 流量控制"></a>2.5 流量控制</h2><h3 id="2-5-1-滑动窗口"><a href="#2-5-1-滑动窗口" class="headerlink" title="2.5.1 滑动窗口"></a>2.5.1 滑动窗口</h3><p>一般说来，我们总是希望数据传输得更快一些。但如果发送方把数据发送得过快，接收方就可能来不及接收，这就会造成数据的丢失。</p>
<p>所谓<strong>流量控制</strong>(flow control)就是让发送方的发送速率不要太快，要让接收方来得及接收。</p>
<p>TCP滑动窗口技术通过动态改变窗口大小来实现对端到端设备之间的数据传输进行流量控制。</p>
<p>如下图，主机A和服务器A之间通过滑动窗口来实现流量控制。为方便理解，此例中只考虑主机A发送数据给服务器A时，服务器A通过滑动窗口进行流量控制。</p>
<p><img src="https://ltyeamin.github.io/imgs/2021/09/20210906114746.png"></p>
<ul>
<li>主机A向服务器发送4个长度为1024字节的数据段，其中主机的窗口大小为4096个字节。</li>
<li>服务器A收到第3个数据段后，缓存区满，第4个数据段被丢弃。</li>
<li>服务器以ACK 3073响应，窗口大小调整为3072，表明服务器的缓冲区只能处理3072个字节的数据段。</li>
<li>于是主机A改变其发送速率，发送窗口大小为3072的数据段。</li>
</ul>
<p>在比如下图中，A向B发送数据，在连接建立时，B告诉了A：“我的接收窗口rwnd = 400”（这里rwnd表示receiver window）。因此，发送方的发送窗口不能超过接收方给出的接收窗口的数值。</p>
<p>请注意，TCP的窗口单位是字节，不是报文段。</p>
<p>TCP连接建立时的窗口协商过程在图中没有显示出来。再设每一个报文段为100字节长，而数据报文段序号的初始值设为1。</p>
<p><img src="https://ltyeamin.github.io/imgs/2021/09/20210906160325.png"></p>
<p>接收方的主机B进行了三次流量控制。第一次把窗口减小到rwnd =300，第二次又减到rwnd = 100，最后减到rwnd = 0，即不允许发送方再发送数据了。这种使发送方暂停发送的状态将持续到主机B重新发出一个新的窗口值为止。</p>
<p>TCP为每一个连接设有一个持续计时器(persistence timer)。只要TCP连接的一方收到对方的零窗口通知，就启动持续计时器。若持续计时器设置的时间到期，就发送一个零窗口探测报文段（仅携带1字节的数据），而对方就在确认这个探测报文段时给出了现在的窗口值。如果窗口仍然是零，那么收到这个报文段的一方就重新设置持续计时器。如果窗口不是零，那么死锁的僵局就可以打破了。</p>
<h3 id="2-5-2-Nagle算法"><a href="#2-5-2-Nagle算法" class="headerlink" title="2.5.2 Nagle算法"></a>2.5.2 Nagle算法</h3><p>应用进程把数据传送到TCP的发送缓存后，剩下的发送任务就由TCP来控制。可以用不同的机制来控制TCP报文段的发送时机：</p>
<ul>
<li>TCP维持一个变量，它等于最大报文段长度MSS。只要缓存中存放的数据达到MSS字节时，就组装成一个TCP报文段发送出去。</li>
<li>由发送方的应用进程指明要求发送报文段，即TCP支持的推送(push)操作</li>
<li>发送方的一个计时器期限到了，这时就把当前已有的缓存数据装入报文段（但长度不能超过MSS）发送出去。</li>
</ul>
<p>何控制TCP发送报文段的时机仍然是一个较为复杂的问题，而在TCP的实现中广泛使用<strong>Nagle算法</strong>。</p>
<blockquote>
<p>若发送应用进程把要发送的数据逐个字节地送到TCP的发送缓存，则发送方就把第一个数据字节先发送出去，把后面到达的数据字节都缓存起来。</p>
<p>当发送方收到对第一个数据字符的确认后，再把发送缓存中的所有数据组装成一个报文段发送出去，同时继续对随后到达的数据进行缓存。</p>
<p>只有在收到对前一个报文段的确认后才继续发送下一个报文段。当数据到达较快而网络速率较慢时，用这样的方法可明显地减少所用的网络带宽。</p>
<p>Nagle算法还规定，当到达的数据已达到发送窗口大小的一半或已达到报文段的最大长度时，就立即发送一个报文段。这样做，就可以有效地提高网络的吞吐量。</p>
</blockquote>
<p>再设想一种情况：</p>
<p>TCP接收方的缓存已满，而交互式的应用进程一次只从接收缓存中读取1个字节（这样就使接收缓存空间仅腾出1个字节），然后向发送方发送确认，并把窗口设置为1个字节（但发送的数据报是40字节长）。</p>
<p>接着，发送方又发来1个字节的数据（请注意，发送方发送的IP数据报是41字节长）。</p>
<p>接收方发回确认，仍然将窗口设置为1个字节。这样进行下去，使网络的效率很低。</p>
<p>要解决这个问题，可以让接收方等待一段时间，使得或者接收缓存已有足够空间容纳一个最长的报文段，或者等到接收缓存已有一半空闲的空间。</p>
<p>只要出现这两种情况之一，接收方就发出确认报文，并向发送方通知当前的窗口大小。此外，发送方也不要发送太小的报文段，而是把数据积累成足够大的报文段，或达到接收方缓存的空间的一半大小。</p>
<h2 id="2-6-可靠传输的工作原理"><a href="#2-6-可靠传输的工作原理" class="headerlink" title="2.6 可靠传输的工作原理"></a>2.6 可靠传输的工作原理</h2><h3 id="2-6-1-停止等待协议"><a href="#2-6-1-停止等待协议" class="headerlink" title="2.6.1 停止等待协议"></a>2.6.1 停止等待协议</h3><p><strong>停止等待</strong>就是每发送完一个分组就停止发送，等待对方的确认。在收到确认后再发送下一个分组。</p>
<p>无差错情况下正常发送-确认即可，直到发完所有分组为止。</p>
<p>有差错情况下，丢弃当前数据，长时间等待接收后无果后进行<strong>超时重传</strong>。</p>
<p>停止等待协议的优点是简单，但缺点是信道利用率太低。</p>
<h3 id="2-6-2-ARQ协议"><a href="#2-6-2-ARQ协议" class="headerlink" title="2.6.2 ARQ协议"></a>2.6.2 ARQ协议</h3><p>ARQ(Automatic Repeat-Request),自動重傳请求，发送窗口内的连续分组，不需要等待对方确认，这样信道利用率就提高了。</p>
<p>接收方一般都是采用累积确认的方式。接收方可以收到几个分组后，对按序到达的最后一个分组发送确认，这就表示：到这个分组为止的所有分组都已正确收到了。</p>
<p>累积确认优点是：容易实现，即使确认丢失也不必重传。但缺点是不能向发送方反映出接收方已经正确收到的所有分组的信息。</p>
<h1 id="3-UDP协议"><a href="#3-UDP协议" class="headerlink" title="3. UDP协议"></a>3. UDP协议</h1><h2 id="3-1-UDP简介"><a href="#3-1-UDP简介" class="headerlink" title="3.1 UDP简介"></a>3.1 UDP简介</h2><p><img src="https://ltyeamin.github.io/imgs/2021/09/20210906162716.png"></p>
<p>当应用程序对传输的可靠性要求不高，但是对传输速度和延迟要求较高时，可以用UDP协议来替代TCP协议在传输层控制数据的转发。 </p>
<p>UDP将数据从源端发送到目的端时，无需事先建立连接。 </p>
<p>UDP采用了简单、易操作的机制在应用程序间传输数据，没有使用TCP中的确认技术或滑动窗口机制，因此UDP不能保证数据传输的可靠性，也无法避免接收到重复数据的情况。</p>
<h2 id="3-2-UDP头部"><a href="#3-2-UDP头部" class="headerlink" title="3.2 UDP头部"></a>3.2 UDP头部</h2><p>UDP报文分为UDP报文头和UDP数据区域两部分。报头由源端口、目的端口、报文长度以及校验和组成。</p>
<p> UDP适合于实时数据传输，如语音和视频通信。</p>
<p>相比于TCP， UDP的传输效率更高、开销更小，但是无法保障数据传输的可靠性。</p>
<p><img src="https://ltyeamin.github.io/imgs/2021/09/20210906162857.png"></p>
<ul>
<li>Source Port: 16位源端口号,源主机的应用程序使用的端口号。</li>
<li>Destination Port： 16位目的端口号，目的主机的应用程序使用的端口号。</li>
<li>Length：16位UDP长度：是指UDP头部和UDP数据的字节长度。因为UDP头部长度为8字节，所以该字段的最小值为8。</li>
<li>Checksum：16位UDP校验和：该字段提供了与TCP校验字段同样的功能；该字段是可选的。</li>
</ul>
<h2 id="3-3-传输过程"><a href="#3-3-传输过程" class="headerlink" title="3.3 传输过程"></a>3.3 传输过程</h2><p><img src="https://ltyeamin.github.io/imgs/2021/09/20210906163248.png"></p>
<p><img src="https://ltyeamin.github.io/imgs/2021/09/20210906163351.png"></p>
<p>主机A发送数据包时，这些数据包是以有序的方式发送到网络中的，每个数据包独立地在网络中被发送，所以不同的数据包可能会通过不同的网络路径到达主机B。</p>
<p>这样的情况下，先发送的数据包不一定先到达主机B。因为UDP数据包没有序号，主机B将无法通过UDP协议将数据包按照原来的顺序重新组合，所以此时需要应用程序提供报文的到达确认、排序和流量控制等功能。</p>
<p>通常情况下， UDP适合传输对如语音和视频等时延敏感的流量，UDP采用实时传输机制和时间戳来传输语音和视频数据。</p>
<p>在使用TCP协议传输数据时，如果一个数据段丢失或者接收端对某个数据段没有确认，发送端会重新发送该数据段。</p>
<p>TCP重新发送数据会带来传输延迟和重复数据，降低了用户的体验。对于时延敏感的应用，少量的数据丢失一般可以被忽略，这时使用UDP传输将能够提升用户的体验。</p>
<h1 id="5-常见服务端口"><a href="#5-常见服务端口" class="headerlink" title="5. 常见服务端口"></a>5. 常见服务端口</h1><table>
<thead>
<tr>
<th align="center">传输层协议</th>
<th align="center">应用服务</th>
<th align="center">默认端口号</th>
</tr>
</thead>
<tbody><tr>
<td align="center">TCP/TCP + TLS/SSL</td>
<td align="center">HTTP/HTTPS</td>
<td align="center">80/443</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">SSH/SCP</td>
<td align="center">22</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">FTP/FTPS</td>
<td align="center">20、21</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">TELNET</td>
<td align="center">23</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">SMTP</td>
<td align="center">25</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">POP3</td>
<td align="center">110</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">SOCKS</td>
<td align="center">1080</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">RDP</td>
<td align="center">3389</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">MySQL</td>
<td align="center">3306</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">Oracle</td>
<td align="center">1521</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">SQL Server</td>
<td align="center">1433</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">Redis</td>
<td align="center">6379</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">Zookeeper</td>
<td align="center">2181、3888、2888</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">Kafka</td>
<td align="center">9092</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">RocketMQ</td>
<td align="center">9876</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">Nacos</td>
<td align="center">8848</td>
</tr>
<tr>
<td align="center">TCP/UDP</td>
<td align="center">ElasticSearch</td>
<td align="center">9200、9300、54328</td>
</tr>
<tr>
<td align="center">TCP/UDP</td>
<td align="center">DNS</td>
<td align="center">53</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">MicroSoft  Share Folders</td>
<td align="center">445</td>
</tr>
<tr>
<td align="center">TCP</td>
<td align="center">rsync</td>
<td align="center">973</td>
</tr>
<tr>
<td align="center">UDP</td>
<td align="center">DHCP</td>
<td align="center">67、68</td>
</tr>
</tbody></table>
<h1 id="6-参考文档"><a href="#6-参考文档" class="headerlink" title="6. 参考文档"></a>6. 参考文档</h1><ul>
<li><a target="_blank" rel="noopener" href="https://e.huawei.com/cn/talent/#/">华为HCIA认证-路由交换方向培训教材</a></li>
<li><a target="_blank" rel="noopener" href="https://book.douban.com/subject/26960678/">谢仁希著《计算机网络》第七版 </a></li>
<li><a target="_blank" rel="noopener" href="https://www.bilibili.com/video/BV15A411s7Da?from=search&seid=293609305123274311&spm_id_from=333.337.0.0">韩立刚计算机网络培训教程</a></li>
</ul>
<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=530 height=86 src="//music.163.com/outchain/player?type=2&id=470777376&auto=1&height=66"></iframe>
</section>
    <!-- Tags START -->
    
      <div class="tags">
        <span>Tags:</span>
        
  <a href="/tags#计算机网络" >
    <span class="tag-code">计算机网络</span>
  </a>

      </div>
    
    <!-- Tags END -->
    <!-- NAV START -->
    
  <div class="nav-container">
    <!-- reverse left and right to put prev and next in a more logic postition -->
    
      <a class="nav-left" href="/2021/09/04/Spring%20Boot%E5%88%9D%E5%A7%8B%E5%8C%96%E7%9A%84%E9%82%A3%E7%82%B9%E4%BA%8B/">
        <span class="nav-arrow">← </span>
        
          Spring Boot初始化的那点事
        
      </a>
    
    
      <a class="nav-right" href="/2021/12/31/%E4%BD%BF%E7%94%A8iText%E5%BA%93%E7%94%9F%E6%88%90PDF%E6%8A%A5%E8%A1%A8/">
        
          使用iText库生成PDF报表
        
        <span class="nav-arrow"> →</span>
      </a>
    
  </div>

    <!-- NAV END -->
    <!-- 打赏 START -->
    
      <div class="money-like">
        <div class="reward-btn">
          赏
          <span class="money-code">
            <span class="alipay-code">
              <div class="code-image"></div>
              <b>使用支付宝打赏</b>
            </span>
            <span class="wechat-code">
              <div class="code-image"></div>
              <b>使用微信打赏</b>
            </span>
          </span>
        </div>
        <p class="notice">若你觉得我的文章对你有帮助，欢迎点击上方按钮对我打赏</p>
      </div>
    
    <!-- 打赏 END -->
    <!-- 二维码 START -->
    
      <div class="qrcode">
        <canvas id="share-qrcode"></canvas>
        <p class="notice">扫描二维码，分享此文章</p>
      </div>
    
    <!-- 二维码 END -->
    
      <!-- Utterances START -->
      <div id="utterances"></div>
      <script src="https://utteranc.es/client.js"
        repo="ltyeamin/blogtalks"
        issue-term="pathname"
        theme="github-light"
        crossorigin="anonymous"
        async></script>    
      <!-- Utterances END -->
    
  </article>
  <!-- Article END -->
  <!-- Catalog START -->
  
    <aside class="catalog-container">
  <div class="toc-main">
    <strong class="toc-title">Catalog</strong>
    
      <ol class="toc-nav"><li class="toc-nav-item toc-nav-level-1"><a class="toc-nav-link" href="#1-%E4%BC%A0%E8%BE%93%E5%B1%82%E6%A6%82%E8%BF%B0"><span class="toc-nav-text">1. 传输层概述</span></a></li><li class="toc-nav-item toc-nav-level-1"><a class="toc-nav-link" href="#2-TCP-IP%E5%8D%8F%E8%AE%AE"><span class="toc-nav-text">2. TCP&#x2F;IP协议</span></a><ol class="toc-nav-child"><li class="toc-nav-item toc-nav-level-2"><a class="toc-nav-link" href="#2-1-TCP%E5%8F%8A%E5%A5%97%E6%8E%A5%E5%AD%97"><span class="toc-nav-text">2.1 TCP及套接字</span></a></li><li class="toc-nav-item toc-nav-level-2"><a class="toc-nav-link" href="#2-2-TCP%E4%B8%BB%E8%A6%81%E7%89%B9%E7%82%B9"><span class="toc-nav-text">2.2 TCP主要特点</span></a></li><li class="toc-nav-item toc-nav-level-2"><a class="toc-nav-link" href="#2-3-TCP%E5%A4%B4%E9%83%A8"><span class="toc-nav-text">2.3 TCP头部</span></a></li><li class="toc-nav-item toc-nav-level-2"><a class="toc-nav-link" href="#2-4-%E8%BF%9E%E6%8E%A5%E7%AE%A1%E7%90%86"><span class="toc-nav-text">2.4 连接管理</span></a><ol class="toc-nav-child"><li class="toc-nav-item toc-nav-level-3"><a class="toc-nav-link" href="#2-4-1-%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B%E8%BF%9E%E6%8E%A5"><span class="toc-nav-text">2.4.1 三次握手连接</span></a></li><li class="toc-nav-item toc-nav-level-3"><a class="toc-nav-link" href="#2-4-2-TCP%E4%BC%A0%E8%BE%93%E8%BF%87%E7%A8%8B"><span class="toc-nav-text">2.4.2  TCP传输过程</span></a></li><li class="toc-nav-item toc-nav-level-3"><a class="toc-nav-link" href="#2-4-3-%E5%9B%9B%E6%AC%A1%E6%8F%A1%E6%89%8B%E9%87%8A%E6%94%BE"><span class="toc-nav-text">2.4.3  四次握手释放</span></a></li></ol></li><li class="toc-nav-item toc-nav-level-2"><a class="toc-nav-link" href="#2-5-%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6"><span class="toc-nav-text">2.5 流量控制</span></a><ol class="toc-nav-child"><li class="toc-nav-item toc-nav-level-3"><a class="toc-nav-link" href="#2-5-1-%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3"><span class="toc-nav-text">2.5.1 滑动窗口</span></a></li><li class="toc-nav-item toc-nav-level-3"><a class="toc-nav-link" href="#2-5-2-Nagle%E7%AE%97%E6%B3%95"><span class="toc-nav-text">2.5.2 Nagle算法</span></a></li></ol></li><li class="toc-nav-item toc-nav-level-2"><a class="toc-nav-link" href="#2-6-%E5%8F%AF%E9%9D%A0%E4%BC%A0%E8%BE%93%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86"><span class="toc-nav-text">2.6 可靠传输的工作原理</span></a><ol class="toc-nav-child"><li class="toc-nav-item toc-nav-level-3"><a class="toc-nav-link" href="#2-6-1-%E5%81%9C%E6%AD%A2%E7%AD%89%E5%BE%85%E5%8D%8F%E8%AE%AE"><span class="toc-nav-text">2.6.1 停止等待协议</span></a></li><li class="toc-nav-item toc-nav-level-3"><a class="toc-nav-link" href="#2-6-2-ARQ%E5%8D%8F%E8%AE%AE"><span class="toc-nav-text">2.6.2 ARQ协议</span></a></li></ol></li></ol></li><li class="toc-nav-item toc-nav-level-1"><a class="toc-nav-link" href="#3-UDP%E5%8D%8F%E8%AE%AE"><span class="toc-nav-text">3. UDP协议</span></a><ol class="toc-nav-child"><li class="toc-nav-item toc-nav-level-2"><a class="toc-nav-link" href="#3-1-UDP%E7%AE%80%E4%BB%8B"><span class="toc-nav-text">3.1 UDP简介</span></a></li><li class="toc-nav-item toc-nav-level-2"><a class="toc-nav-link" href="#3-2-UDP%E5%A4%B4%E9%83%A8"><span class="toc-nav-text">3.2 UDP头部</span></a></li><li class="toc-nav-item toc-nav-level-2"><a class="toc-nav-link" href="#3-3-%E4%BC%A0%E8%BE%93%E8%BF%87%E7%A8%8B"><span class="toc-nav-text">3.3 传输过程</span></a></li></ol></li><li class="toc-nav-item toc-nav-level-1"><a class="toc-nav-link" href="#5-%E5%B8%B8%E8%A7%81%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%8F%A3"><span class="toc-nav-text">5. 常见服务端口</span></a></li><li class="toc-nav-item toc-nav-level-1"><a class="toc-nav-link" href="#6-%E5%8F%82%E8%80%83%E6%96%87%E6%A1%A3"><span class="toc-nav-text">6. 参考文档</span></a></li></ol>
    
  </div>
</aside>
  
  <!-- Catalog END -->
</main>

<script>
  (function () {
    var url = 'http://example.com/2021/09/05/跟着彤哥学网络-传输层TCP&amp;UDP协议/';
    var banner = ''
    if (banner !== '' && banner !== 'undefined' && banner !== 'null') {
      $('#article-banner').css({
        'background-image': 'url(' + banner + ')'
      })
    } else {
      $('#article-banner').geopattern(url)
    }
    $('.header').removeClass('fixed-header')

    // error image
    $(".markdown-content img").on('error', function() {
      $(this).attr('src', '/css/images/error_icon.png')
      $(this).css({
        'cursor': 'default'
      })
    })

    // zoom image
    $(".markdown-content img").on('click', function() {
      var src = $(this).attr('src')
      if (src !== '/css/images/error_icon.png') {
        var imageW = $(this).width()
        var imageH = $(this).height()

        var zoom = ($(window).width() * 0.95 / imageW).toFixed(2)
        zoom = zoom < 1 ? 1 : zoom
        zoom = zoom > 2 ? 2 : zoom
        var transY = (($(window).height() - imageH) / 2).toFixed(2)

        $('body').append('<div class="image-view-wrap"><div class="image-view-inner"><img src="'+ src +'" /></div></div>')
        $('.image-view-wrap').addClass('wrap-active')
        $('.image-view-wrap img').css({
          'width': `${imageW}`,
          'transform': `translate3d(0, ${transY}px, 0) scale3d(${zoom}, ${zoom}, 1)`
        })
        $('html').css('overflow', 'hidden')

        $('.image-view-wrap').on('click', function() {
          $(this).remove()
          $('html').attr('style', '')
        })
      }
    })
  })();
</script>


  <script>
    var qr = new QRious({
      element: document.getElementById('share-qrcode'),
      value: document.location.href
    });
  </script>






    <div class="scroll-top">
  <span class="arrow-icon"></span>
</div>
    <footer class="app-footer">
  <p class="copyright">
    &copy; 2024 | Proudly powered by <a href="https://hexo.io" target="_blank">Hexo</a>
    <br>
    Theme by <a target="_blank" rel="noopener" href="https://github.com/ltyeamin">tong.li</a>
  </p>
</footer>

<script>
  function async(u, c) {
    var d = document, t = 'script',
      o = d.createElement(t),
      s = d.getElementsByTagName(t)[0];
    o.src = u;
    if (c) { o.addEventListener('load', function (e) { c(null, e); }, false); }
    s.parentNode.insertBefore(o, s);
  }
</script>
<script>
  async("https://cdn.staticfile.org/fastclick/1.0.6/fastclick.min.js", function(){
    FastClick.attach(document.body);
  })
</script>

<script>
  var hasLine = 'true';
  async("https://cdn.staticfile.org/highlight.js/9.12.0/highlight.min.js", function(){
    $('figure pre').each(function(i, block) {
      var figure = $(this).parents('figure');
      if (hasLine === 'false') {
        figure.find('.gutter').hide();
      }
      hljs.configure({useBR: true});
      var lang = figure.attr('class').split(' ')[1] || 'code';
      var codeHtml = $(this).html();
      var codeTag = document.createElement('code');
      codeTag.className = lang;
      codeTag.innerHTML = codeHtml;
      $(this).attr('class', '').empty().html(codeTag);
      figure.attr('data-lang', lang.toUpperCase());
      hljs.highlightBlock(block);
    });
  })
</script>
<!-- Baidu Tongji -->



<script src='https://cdn.staticfile.org/mermaid/8.11.2/mermaid.min.js'></script>



<script src="/js/script.js"></script>


  </body>
</html>